// 《围棋》作者版权所有。保留所有权利。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

// go:build plan9 

package lockedfile

import (
	"io/fs"
	"math/rand"
	"os"
	"strings"
	"time"
)

// 打开专用文件会返回错误。
// 预期的错误字符串是：
// 
// -“打开/创建--文件已锁定”（cwfs，kfs）
// -“独占锁定”（化石）
// -“独占使用文件已打开”（ramfs）
var lockedErrStrings = [...]string{
	"file is locked",
	"exclusive lock",
	"exclusive use file already open",
}

// 尽管plan9不支持锁定/锁定/解锁函数来操作已打开的文件，IsLocked仍然有意义：操作系统。OpenFile 
// 本身可能会返回错误，表明设置了ModeExclusive位
// 的文件已打开。huang jian defg
func isLocked(err error) bool {
	s := err.Error()

	for _, frag := range lockedErrStrings {
		if strings.Contains(s, frag) {
			return true
		}
	}

	return false
}

func openFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
	// 
	// Per http:
	// 一次只能通过一个fid在服务器的所有客户端上进行I/O。如果尝试第二次打开
	// ，则会出现错误。”
	// 
	// 因此我们可以尝试打开一个锁定的文件，但如果失败，我们将自行决定何时可用。我们将使用
	// 一些抖动和500毫秒的任意限制的指数回退。

	// 如果文件是解包的或由其他程序创建的，则可能不会设置MODE EXCLUSTIC位。在调用OpenFile之前设置它，这样我们就可以确信一个成功的OpenFile意味着独占使用。
	if fi, err := os.Stat(name); err == nil {
		if fi.Mode()&fs.ModeExclusive == 0 {
			if err := os.Chmod(name, fi.Mode()|fs.ModeExclusive); err != nil {
				return nil, err
			}
		}
	} else if !os.IsNotExist(err) {
		return nil, err
	}

	nextSleep := 1 * time.Millisecond
	const maxSleep = 500 * time.Millisecond
	for {
		f, err := os.OpenFile(name, flag, perm|fs.ModeExclusive)
		if err == nil {
			return f, nil
		}

		if !isLocked(err) {
			return nil, err
		}

		time.Sleep(nextSleep)

		nextSleep += nextSleep
		if nextSleep > maxSleep {
			nextSleep = maxSleep
		}
		// 应用10%的抖动以避免同步冲突。
		nextSleep += time.Duration((0.1*rand.Float64() - 0.05) * float64(nextSleep))
	}
}

func closeFile(f *os.File) error {
	return f.Close()
}
